@file:Suppress("UNUSED")

package kotlinx.coroutines.android

import android.os.Handler
import android.os.Looper
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Delay
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.MainCoroutineDispatcher
import kotlinx.coroutines.NonDisposableHandle
import kotlinx.coroutines.cancel
import kotlinx.coroutines.internal.MainDispatcherFactory
import kotlin.coroutines.CoroutineContext

/**
 * Dispatches execution onto Android [Handler].
 *
 * This class provides type-safety and a point for future extensions.
 */
@OptIn(InternalCoroutinesApi::class)
public sealed class HandlerDispatcher :
    MainCoroutineDispatcher(),
    Delay {
    /**
     * Returns dispatcher that executes coroutines immediately when it is already in the right context
     * (current looper is the same as this handler's looper) without an additional [re-dispatch][CoroutineDispatcher.dispatch].
     * This dispatcher does not use [Handler.post] when current looper is the same as looper of the handler.
     *
     * Immediate dispatcher is safe from stack overflows and in case of nested invocations forms event-loop similar to [Dispatchers.Unconfined].
     * The event loop is an advanced topic and its implications can be found in [Dispatchers.Unconfined] documentation.
     *
     * Example of usage:
     * ```
     * suspend fun updateUiElement(val text: String) {
     *     /*
     *      * If it is known that updateUiElement can be invoked both from the Main thread and from other threads,
     *      * `immediate` dispatcher is used as a performance optimization to avoid unnecessary dispatch.
     *      *
     *      * In that case, when `updateUiElement` is invoked from the Main thread, `uiElement.text` will be
     *      * invoked immediately without any dispatching, otherwise, the `Dispatchers.Main` dispatch cycle via
     *      * `Handler.post` will be triggered.
     *      */
     *     withContext(Dispatchers.Main.immediate) {
     *         uiElement.text = text
     *     }
     *     // Do context-independent logic such as logging
     * }
     * ```
     */
    public abstract override val immediate: HandlerDispatcher
}

@OptIn(InternalCoroutinesApi::class)
internal class AndroidDispatcherFactory : MainDispatcherFactory {
    override fun createDispatcher(allFactories: List<MainDispatcherFactory>): MainCoroutineDispatcher {
        val mainLooper = Looper.getMainLooper() ?: throw IllegalStateException("The main looper is not available")
        return HandlerContext(mainLooper.asHandler())
    }

    override fun hintOnError(): String = "For tests Dispatchers.setMain from kotlinx-coroutines-test module can be used"

    override val loadPriority: Int
        get() = Int.MAX_VALUE / 2
}

/**
 * Represents an arbitrary [Handler] as an implementation of [CoroutineDispatcher]
 * with an optional [name] for nicer debugging
 *
 * ## Rejected execution
 *
 * If the underlying handler is closed and its message-scheduling methods start to return `false` on
 * an attempt to submit a continuation task to the resulting dispatcher,
 * then the [Job] of the affected task is [cancelled][Job.cancel] and the task is submitted to the
 * [Dispatchers.IO], so that the affected coroutine can cleanup its resources and promptly complete.
 */
@JvmName("from") // this is for a nice Java API, see issue #255
@JvmOverloads
public fun Handler.asCoroutineDispatcher(name: String? = null): HandlerDispatcher =
    HandlerContext(this, name)

private const val MAX_DELAY = Long.MAX_VALUE / 2 // cannot delay for too long on Android

internal fun Looper.asHandler(): Handler = Handler(this)

@JvmField
@Deprecated("Use Dispatchers.Main instead", level = DeprecationLevel.HIDDEN)
internal val Main: HandlerDispatcher? = runCatching { HandlerContext(Looper.getMainLooper().asHandler()) }.getOrNull()

/**
 * Implements [CoroutineDispatcher] on top of an arbitrary Android [Handler].
 */
@OptIn(InternalCoroutinesApi::class)
internal class HandlerContext private constructor(
    private val handler: Handler,
    private val name: String?,
    private val invokeImmediately: Boolean,
) : HandlerDispatcher(),
    Delay {
    /**
     * Creates [CoroutineDispatcher] for the given Android [handler].
     *
     * @param handler a handler.
     * @param name an optional name for debugging.
     */
    constructor(
        handler: Handler,
        name: String? = null,
    ) : this(handler, name, false)

    override val immediate: HandlerContext =
        if (invokeImmediately) {
            this
        } else {
            HandlerContext(handler, name, true)
        }

    override fun isDispatchNeeded(context: CoroutineContext): Boolean = !invokeImmediately || Looper.myLooper() != handler.looper

    override fun dispatch(
        context: CoroutineContext,
        block: Runnable,
    ) {
        if (!handler.post(block)) {
            cancelOnRejection(context, block)
        }
    }

    @OptIn(ExperimentalCoroutinesApi::class)
    override fun scheduleResumeAfterDelay(
        timeMillis: Long,
        continuation: CancellableContinuation<Unit>,
    ) {
        val block =
            Runnable {
                with(continuation) { resumeUndispatched(Unit) }
            }
        if (handler.postDelayed(block, timeMillis.coerceAtMost(MAX_DELAY))) {
            continuation.invokeOnCancellation { handler.removeCallbacks(block) }
        } else {
            cancelOnRejection(continuation.context, block)
        }
    }

    override fun invokeOnTimeout(
        timeMillis: Long,
        block: Runnable,
        context: CoroutineContext,
    ): DisposableHandle {
        if (handler.postDelayed(block, timeMillis.coerceAtMost(MAX_DELAY))) {
            return DisposableHandle { handler.removeCallbacks(block) }
        }
        cancelOnRejection(context, block)
        return NonDisposableHandle
    }

    private fun cancelOnRejection(
        context: CoroutineContext,
        block: Runnable,
    ) {
        context.cancel(CancellationException("The task was rejected, the handler underlying the dispatcher '${toString()}' was closed"))
        Dispatchers.IO.dispatch(context, block)
    }

    override fun toString(): String =
        toStringInternalImpl() ?: run {
            val str = name ?: handler.toString()
            if (invokeImmediately) "$str.immediate" else str
        }

    override fun equals(other: Any?): Boolean =
        other is HandlerContext && other.handler === handler && other.invokeImmediately == invokeImmediately

    // inlining `Boolean.hashCode()` for Android compatibility, as requested by Animal Sniffer
    override fun hashCode(): Int = System.identityHashCode(handler) xor if (invokeImmediately) 1231 else 1237
}
